/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions anmcd
 * limitations under the License.
 */
import * as fs from 'fs'
import path from 'path'
import { BaseType, SRClassType, SRFile, SRScene } from './SRScene';
import { SRConfig } from './SRConfig';
import { genClass, genClassFactory, genEnumHelper } from './classTemplates';
import { gitIgnorePattern, defaultArkClassName, classFactory } from './nameUtils';

import Logger from "./logger";

const logger = Logger.getLogger();

function clearGitIgnoreContent(filePath: string): void {
    if (!fs.existsSync(filePath)) {
        return;
    }
    const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
    const newLines = [];
    let startLine = -1;
    let endLine = -1;

    for (let i = 0; i < lines.length; i++) {
        if (lines[i] == gitIgnorePattern) {
            if (startLine == -1) {
                startLine = i;
            } else if (endLine == -1) {
                endLine = i;
            } else {
                logger.warn("something wrong with content generated by sendable-class transformer in gitignore!");
            }
        }
        if (!(startLine != -1 && i >= startLine && (endLine == -1 || i <= endLine))) {
            newLines.push(lines[i]);
        }
    }

    fs.writeFileSync(filePath, newLines.join('\n'), 'utf-8');
}

export function handleError(errorMsg: string, config: SRConfig) {
    Logger.getLogger().error(errorMsg);
    if (config.isAbortOnConvertError()) {
        throw new Error(errorMsg);
    }
}

export function codeGen(scene: SRScene, config: SRConfig) {
    const gitignoreFilePath = path.dirname(config.getTtsConfigJsonPath()) + path.sep + '.gitignore';
    if (config.needAddNewFileToGitIgnore()) {
        try {
            // firstly clear previous generated ets file into gitignore
            clearGitIgnoreContent(gitignoreFilePath);
            // add gitIgnorePattern before adding ets file into gitignore
            if (fs.existsSync(gitignoreFilePath)) {
                fs.appendFileSync(gitignoreFilePath, '\n' + gitIgnorePattern);
            } else {
                fs.writeFileSync(gitignoreFilePath, '\n' + gitIgnorePattern);
            }
        } catch (error) {
            handleError(`Error clearing previous gitignore content or adding gitignore pattern! Error msg: ${error}`, config);
        }
    }

    try {
        scene.getFiles().forEach(file => {
            const result = genFile(file, config);
            if(!result.ok){
                handleError(`Error generating file: ${result.reason}`, config);
                return;
            }
            let filePath = file.getFilePath().replace(/(.*)\.tts/, '$1.ets')
            fs.mkdir(path.dirname(filePath), {recursive: true}, (err) => {
                if(err) {
                    handleError(`Error creating directory for generated ${filePath}! Error msg: ${err}`, config);
                    return;
                }
                fs.writeFile(filePath, result.content, (err) => {
                    if(err) {
                        handleError(`Error creating file ${filePath}! Error msg: ${err}`, config);
                        return;
                    }
                })
            });
            
            try {
                let filePathRelativeToConfig: string = filePath.replace(path.dirname(config.getTtsConfigJsonPath()), '');
                filePathRelativeToConfig = filePathRelativeToConfig.split(path.sep).join('/');
                if (result.ok && config.needAddNewFileToGitIgnore()) {
                    logger.debug(`generate ets file for ${filePath} success!`);
                    fs.appendFileSync(gitignoreFilePath, '\n' + filePathRelativeToConfig);
                }
            } catch(error) {
                handleError(`Error adding generated ets files to gitignore! Error msg: ${error}`, config);
            }
        })
    } catch (error) {
        throw error;
    }

    // add gitIgnorePattern after all of the generated ets files
    if (config.needAddNewFileToGitIgnore()) {
        try {
            fs.appendFileSync(gitignoreFilePath, '\n' + gitIgnorePattern);
        } catch (error) {
            handleError(`Error adding gitignore pattern afther adding the last of generated ets files! Error msg: ${error}`, config);
        }
    }
}

type GenFileResult = {
    ok: true,
    path: string,
    content: string,
} | {
    ok: false,
    path: string,
    reason: string,
}

function handleImports(file: SRFile, config:SRConfig): string {
    let isCollectionUsed = false;
    let isObjectUsed = false;
    let clsImports:string[] = [];
    file.getClasses().forEach(cls=>{
        cls.getFields().forEach(field=>{
            if ([BaseType.OBJECT, BaseType.MAP, BaseType.COLLECTIONS_MAP].includes(field.getType().category)
             || field.getType().genericTypes.find(type=>type.category===BaseType.OBJECT)) {
                isObjectUsed = true;
            }
            const nestedTypes = [BaseType.ARRAY, BaseType.COLLECTIONS_ARRAY, BaseType.COLLECTIONS_MAP,
                BaseType.COLLECTIONS_SET, BaseType.MAP, BaseType.SET];
            if (nestedTypes.includes(field.getType().category)) {
                isCollectionUsed = true;
            }
            if (field.getType().category===BaseType.CLASS || field.getType().genericTypes.find(type=>type.category===BaseType.CLASS)
            || field.getType().category===BaseType.OTHERS || field.getType().genericTypes.find(type=>type.category===BaseType.OTHERS)) {
                clsImports.push(field.getType().name)
            }
        })
    });
    const importsMap = rebuildImports(file);
    if (isCollectionUsed) {
        addImportsToMap(importsMap, "@kit.ArkTS", "collections");
    }
    if (isObjectUsed) {
        addImportsToMap(importsMap, "@kit.ArkTS", "lang");
    }
    addImportsToMap(importsMap, "sendable-transformer", "ISerializable");
    addImportsToMap(importsMap, "sendable-transformer", "ISerializableType");
    let importExpr = Array.from(importsMap).filter(([key,value])=>key!='class-transformer').map(([key,value])=>{
        return `import { ${value.filter(val=>val!==undefined).flatMap(val=>{
                if(typeof val === 'string') {
                    return val;
                }else if(val instanceof Set){
                    return `${Array.from(val).flatMap(v=>{
                        if(clsImports.includes(v)) {
                            return [v, classFactory(v)];
                        }else{
                            return v;
                        }
                    }).join(", ")}`
                }
            }).join(", ")
        } } from '${key}';`
    }).join("\n")
    return `${importExpr}\n${config.getCopyImportPrefixes().join('\n')}`
}

function genFile(file: SRFile, config: SRConfig): GenFileResult {
    try{
        const content = `${handleImports(file, config)}

${file.getClasses().filter(cls => cls.getName() !== defaultArkClassName).flatMap(cls => {
    if(cls.getType()===SRClassType.ENUM) {
        return genEnumHelper(cls)
    } else if (cls.getMethods().find(method => method.getName() == 'constructor' && method.getParameters().length > 0)) {
        logger.info(`Class ${cls.getName()} has constructor with parameters, transformed @Sendable class will remain the same!`);
        return [ cls.getOriginalCode(), '' ]
    } else {
        return [ genClass(cls), genClassFactory(cls) ]
    }
}).join('\n\n')}
        `;
        return {
            ok: true,
            path: file.getFilePath(),
            content,
        };
    } catch (error) {
        return {
            ok: false,
            path: file.getFilePath(),
            reason:(error as Error).message,
        };
    }

}

function rebuildImports(file: SRFile): Map<string, [Set<string>, string| undefined]>{
    const importInfoMap = new Map<string, [Set<string>, string| undefined]>();
    Array.from(file.getImportInfoMap()).forEach(([key,value])=>{
        const unNamedImport = value.find(v=>!v.isNamedImport())?.getElementName();
        const importSet = new Set<string>(value.filter(v=>v.isNamedImport()).map(v=>v.getElementName()));
        const importInfo:[Set<string>, string| undefined] = [importSet, unNamedImport]
        importInfoMap.set(key,importInfo)
    })
    return importInfoMap;
}
function addImportsToMap(map:Map<string, [Set<string>, string| undefined]>, importSource: string, elementName: string) {
    if (map.has(importSource)) {
        const value = map.get(importSource)!;
        if (value[1]===undefined || value[1]!==elementName) {
            value[0].add(elementName);
        } 
        if (value[1]===elementName) {
            throw new Error(`generate imports error: import ${elementName} from '${importSource}' is not supported, please use import {${elementName}} from '${importSource}'`);
        }
    }else{
        map.set(importSource, [new Set<string>([elementName]), undefined])
    }
}
